fix: trim-compatibility for Router registration, the precompile workload, and one error path#1330
fix: trim-compatibility for Router registration, the precompile workload, and one error path#1330quinnj wants to merge 8 commits into
Conversation
… and one error path Three small changes that make HTTP usable in juliac --trim builds: - register!/Router: add type parameters to the handler/middleware arguments. Julia doesn't specialize on function-valued arguments that are only passed through, so every registration funneled into one handler::Function instance where the middleware wrap and parametric Leaf/Router construction are runtime apply_type calls — unresolvable under --trim (6 verifier errors in a real server graph). - precompile workload: HTTP_PRECOMPILE_WORKLOAD=0 env opt-out. The workload starts real servers/requests, which segfaults when executed inside a static trim compilation. - http_client: don't interpolate plan.mode into the HTTP/2 proxy ArgumentError — enum show machinery is a dynamic site under --trim.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #1330 +/- ##
==========================================
+ Coverage 88.04% 88.07% +0.02%
==========================================
Files 30 30
Lines 11849 12016 +167
==========================================
+ Hits 10433 10583 +150
- Misses 1416 1433 +17 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
The h2 GOAWAY request was the only shutdown hook ever installed, but the
Union{Nothing,Function} field made every hook invocation dynamic dispatch —
unresolvable under juliac --trim. Hoist _H2ServerConnControl ahead of
_ServerConn and store the hook as a concrete callable struct instead of a
closure.
Deep middleware graphs widen the handler's response type (inference-budget
truncation), turning the response-write calls into runtime specialization
dispatch. _write_all_response!/write_response!/_write_h2_response! (and the
h2 streaming helper) now take @nospecialize(response) — one instance covers
every Response{B} — with an explicit isa chain over the body shapes in
write_response! and socket-union isa splits (with per-branch typeasserts,
which prevent the optimizer from tail-merging the branches back into one
dynamic call) at the h1 call sites.
With the response @nospecialize'd, every body read is abstract; concrete-first
isa chains (String / SubString{String} / Vector{UInt8} / BytesBody{Vector{UInt8}}
/ EmptyBody, then the abstract residuals) keep the common paths statically
dispatched under juliac --trim. The h2 fast path normalizes buffered bodies to
a concrete Vector{UInt8} once (one copy for string bodies; the frame writer
copies into the connection buffer anyway). Shared _body_close_any! replaces the
scattered widened close sites. Static messages in the unsupported-body throws
(typeof interpolation is show machinery — dynamic under trim).
…nospecialized write paths
Nospecializing the write path traded 4 boundary errors for ~90 interior ones
(every body read abstract) and the contract-tightening attempt broke range
serving. Instead: the write paths stay fully specialized per Response{B}
(clean under --trim, verified in isolation), and thin @noinline shims at the
two server boundaries isa-chain over the concrete Response shapes servers
produce — each branch dispatching into the specialized path, one residual
dynamic arm for exotic body types.
…im workloads Review feedback (PR #1330): the per-site isa chains were fragile — easy to miss a branch or let sites drift apart. Two @inline helpers are now the single source of truth for the concrete shapes the server write paths dispatch over: - _with_body_narrowed(f, body): nothing / String / SubString{String} / Vector{UInt8} / EmptyBody / BytesBody{Vector{UInt8}} / AbstractBody (streaming residual) / fallback. Every branch re-narrows so f's call is statically dispatched. Used by _response_has_body (via shared _response_body_known_empty methods, also reused for the h2 body_empty check), the h1 chunked + exact-body writes, the h2 byte normalization, the h2 streaming writer, and _body_close_any!. - _with_response_narrowed(f, response): the Response{B} equivalent, used by both _write_all_response_dyn! / _write_h2_response_dyn! shims. The h2 streaming writer's isa ladder became per-shape _write_h2_body_shape! methods (concrete b from the helper keeps their selection static). New trim-compile workloads in the existing harness (both zero-tolerance, compile + run under stock juliac): - http_trim_server_response_shapes.jl: every supported body shape served and verified over the wire, through the widened-response shims. - http_trim_server_router_registration.jl: Router construction + every register! form incl. middleware wrap and wildcard/param paths. Serving THROUGH the router stays uncovered pending the router-dispatch design. Full-graph verifier count unchanged; h1+h2 server testsets green.
|
Both review points addressed in 26c9b73: Consolidated narrowing helpers — the per-site isa chains are gone. Two
Adding a supported shape is now one branch in one place. Trim-compile tests — turns out master already had the trim harness + 13 client workloads, so I added two server-write workloads to it (both zero-tolerance: compile with 0 verifier errors AND run the binary with wire-level assertions, under stock juliac):
Validation: both new workloads pass locally ( 🤖 Generated with Claude Code |
… pattern) The matched-handler invocation was runtime dispatch over the open set of registered handler closure types — the one remaining trim frontier in the server. Leaf now stores a HandlerFn: a per-callable-type @generated @cfunction whose first C argument is the callable itself (Ref{F}), so the handler call inside the trampoline is concretely dispatched, invocation is a ccall through the stored pointer (statically resolvable), and _root keeps the callable alive. The handler table stays runtime-mutable and register! is unchanged. Two constraints discovered and encoded: - the trim verifier rejects boxed-Any cfunction signatures, so the request and result cross the trampoline as raw pointers to caller-GC.@preserve'd Ref{Any} boxes (primitive-only C signature); - an Any-typed call of the callable is itself specialization dispatch, so _call_handler_narrowed isa-narrows over the closed set of server-side request/stream shapes before invoking. getparams/getparam: typed via an isa narrow of the context fetch (their Any-typed returns made every param use downstream dynamic). The router trim workload now serves THROUGH the router (params, wildcards, 404/405 verified over the wire) — the frontier case is covered and zero-tolerance under the stock harness.
…trim coverage
- "trim_strict_bodies" Preference (default off): juliac --trim builds opt
into strict narrowing — unknown request/response body shapes throw instead
of taking the residual dynamic-dispatch fallback, letting the verifier
prove the narrowing helpers fully static. A Preference rather than an ENV
switch so flipping it invalidates the precompile cache (same pattern as
StructUtils' trim_specialize).
- narrowing chains gain the BytesBody{Base.CodeUnits{UInt8,String}} arms —
what the compat constructors produce for string bodies; previously these
fell through to the AbstractBody/dynamic arms, which strict mode forbids
- response-shapes trim workload gains the 2-arg Response(status, body) form
(the common error-response pattern), and the harness gains a strict-mode
case: the same workload compiled in a temp project with the preference on
(HTTP_TRIM_ONLY=strict selects just this case)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three small changes that make HTTP servers compilable with
juliac --trim:1.
register!/Router: force specialization on handler/middleware argumentsJulia doesn't specialize on function-valued arguments that are only passed through (not called), so every registration funneled into one
handler::Functioninstance — where the middleware wrap and the parametricLeaf/Routerconstructions are runtimeapply_typecalls, unresolvable under--trim(6 verifier errors in a real server graph). Adding::F ... where Fto theregister!forms and theRouterconvenience constructor is dispatch-neutral and makes each registration site compile concretely.Also: build
segmentsas aUnion{String,Variable}container instead ofmap(whose eltype widens toAnyhere) — otherwise every segment comparison insideinsert!is a dynamic==.2.
HTTP_PRECOMPILE_WORKLOAD=0opt-out for the precompile workloadThe workload starts real servers and requests, which must not execute inside a static trim compilation (observed: segfault when the workload runs under
juliac --trim=safe). An explicit env opt-out lets build pipelines disable it without patching the package.3. Don't interpolate
plan.modeinto the HTTP/2 proxyArgumentErrorEnum
showmachinery is a dynamic site under--trim; the static message carries the same information for this error class.Validation
test/http_handlers_tests.jlgreen on this branch.juliac --trim=safeagainst this branch with zero HTTP-rooted verifier errors (down from 6/12), with registration through middleware-wrapped closures and two routers.🤖 Generated with Claude Code